/*
 * Copyright 2023 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.firebase.gradle.plugins

import java.io.File

// TODO(b/281873509) - Add documentation for CHANGELOG.md
/**
 * Type safe representation of `CHANGELOG.md` files.
 *
 * For more information on what a `CHANGELOG.md` file entails, see the following documentation:
 * [Changelog Files](#)
 *
 * @property releases previous releases and the data associated with them
 * @see fromFile
 * @see fromString
 */
data class Changelog(val releases: List<ReleaseEntry>) {

  override fun toString(): String = releases.joinToString("\n") + "\n"

  companion object {
    /**
     * Regex for finding the version titles in a changelog file.
     *
     * The regex can be described as such:
     * - Find lines that start with `#`, followed by a space and any amount of characters
     *
     * For example:
     * ```markdown
     * # Unreleased
     *
     * # 18.3.7
     * [feature] Added collection of version control information generated by the
     *   Android Gradle plugin (AGP).
     *
     * # 18.3.6
     * [feature] Added support for upcoming [crashlytics] features to report
     *   GWP-ASan crashes on supported API levels.
     * ```
     *
     * Will find the following groups:
     * ```kotlin
     * [
     *   "Unreleased",
     *   "18.3.7",
     *   "18.3.6"
     * ]
     * ```
     */
    val RELEASE_VERSION_REGEX = Regex("^# (.+)", RegexOption.MULTILINE)

    /**
     * Parses a [Changelog] from a [String].
     *
     * For example, the following is a valid string:
     * ```markdown
     * # Unreleased
     *
     * # 18.3.7
     * [feature] Added collection of version control information generated by the
     *   Android Gradle plugin (AGP).
     *
     * ## Kotlin
     * The Kotlin extensions library transitively includes the updated
     * `firebase-crashlytics` library. The Kotlin extensions library has no additional
     * updates.
     *
     * # 18.3.6
     * [feature] Added support for upcoming [crashlytics] features to report
     *   GWP-ASan crashes on supported API levels.
     * ```
     *
     * @see RELEASE_VERSION_REGEX
     * @see ReleaseEntry
     */
    fun fromString(string: String): Changelog {
      val content = RELEASE_VERSION_REGEX.split(string).map { it.trim('\n', ' ') }.drop(1)
      val releaseNames =
        RELEASE_VERSION_REGEX.findAll(string).map { it.firstCapturedValue }.toList()

      val releases = releaseNames.zip(content).map { ReleaseEntry.fromString(it.first, it.second) }

      return Changelog(releases)
    }

    /**
     * Parses a [Changelog] from the text content of a [File].
     *
     * Just a helper method to call off to [Changelog.fromString].
     */
    fun fromFile(file: File): Changelog = fromString(file.readText())
  }
}

/**
 * A release in a [Changelog], and its associated data.
 *
 * @property version the [ModuleVersion] that this release was for
 * @property content what went out in the release
 * @property ktx a variant of [content] for a ktx subcategory- if present, or null otherwise
 * @see fromString
 */
data class ReleaseEntry(
  val version: ModuleVersion?,
  val content: ReleaseContent,
  val ktx: ReleaseContent?,
) {

  override fun toString(): String {
    return """
      |# ${version ?: "Unreleased"}
      |$content
      |${ktx?.let { """
      |
      |## Kotlin
      |$it
      |""" }.orEmpty()}
    """
      .trimMargin()
  }

  /**
   * If there is any content in this release.
   *
   * Meaning that there is content in either the [base release][content] or [ktx].
   *
   * @see ReleaseContent.hasContent
   */
  fun hasContent() = content.hasContent() || ktx?.hasContent() ?: false

  companion object {

    /**
     * A static instance of a [ReleaseEntry] without any content.
     *
     * This exists to provide a means for tooling to create new sections explicitly, versus offering
     * default values to [ReleaseEntry]
     * - as this could lead to edge case scenarios where empty [ReleaseEntry] instances are
     *   accidentally created.
     */
    val Empty = ReleaseEntry(null, ReleaseContent("", emptyList()), null)

    /**
     * Regex for finding the Kotlin header in a changelog file.
     *
     * The regex can be described as such:
     * - Look for a line that only contains `## Kotlin`
     *
     * For example:
     * ```markdown
     * # 18.3.7
     * [feature] Added collection of version control information generated by the
     *   Android Gradle plugin (AGP).
     *
     * ## Kotlin
     * [feature] Added support for upcoming [crashlytics] features to report
     *   GWP-ASan crashes on supported API levels.
     * ```
     *
     * Will find the following match:
     * ```kotlin
     * "## Kotlin"
     * ```
     */
    val KOTLIN_TITLE_REGEX = Regex("^## Kotlin\n", RegexOption.MULTILINE)

    /**
     * Parses a [ReleaseEntry] from a [String], given the version name.
     *
     * For example, the following is a valid string:
     * ```markdown
     * [feature] Added some cool stuff
     *
     * [changed] Changed some bad stuff
     *
     * ## Kotlin
     * [feature] Added other cool stuff
     * ```
     *
     * @param versionString the version of the release
     * @param string the content of a release
     * @see KOTLIN_TITLE_REGEX
     * @see ReleaseContent
     */
    fun fromString(versionString: String, string: String): ReleaseEntry {
      val (contentString, ktxString) = KOTLIN_TITLE_REGEX.split(string).toPairOrFirst()

      val content = ReleaseContent.fromString(contentString)
      val ktx = ktxString?.let { ReleaseContent.fromString(it) }
      val version = ModuleVersion.fromStringOrNull(versionString)

      if (ktx?.hasContent() == false)
        throw RuntimeException("KTX header found without any content:\n $string")

      return ReleaseEntry(version, content, ktx)
    }
  }
}

/**
 * The data for a [release][ReleaseEntry].
 *
 * @property subtext optional additional text provided for a release
 * @property changes a list of changes that went out in this release
 * @see fromString
 */
data class ReleaseContent(val subtext: String, val changes: List<Change>) {

  override fun toString(): String {
    val changes = changes.joinToString("\n")

    return when {
      subtext.isNotBlank() && changes.isNotBlank() -> "$subtext\n\n$changes"
      subtext.isNotBlank() -> subtext
      changes.isNotBlank() -> changes
      else -> ""
    }
  }

  /**
   * If there is any content in this release.
   *
   * Meaning that there is either [changes] or [subtext] present.
   */
  fun hasContent() = changes.isNotEmpty() || subtext.isNotBlank()

  companion object {
    /**
     * Regex for finding the changes in a release.
     *
     * The regex can be described as such:
     * - Look for a line that starts with a `*`, followed by a space and any amount of characters
     * - Keep collecting characters until you find a line that starts with `*` or you hit EOF
     *
     * For example:
     * ```markdown
     * This release contains a known bug. We will address this in a future bugfix.
     *
     * [feature] Added some cool stuff
     *
     * [changed] Changed some bad stuff
     * ```
     *
     * Will find the following groups:
     * ```kotlin
     * [
     *   "[feature] Added some cool stuff",
     *   "[changed] Changed some bad stuff"
     * ]
     * ```
     */
    val CHANGE_REGEX = "^[*-] ([\\s\\S]+?)(?=^[*-]|(?![\\s\\S]))".toRegex(RegexOption.MULTILINE)

    /**
     * Regex for finding the subtext in a release.
     *
     * The regex can be described as such:
     * - Look for a line that with any amount of characters
     * - This line should not start with whitespace or a `*`
     * - This line should either end with two new lines or there should be nothing after it
     *
     * For example:
     * ```markdown
     * This release contains a known bug. We will address this in a future bugfix.
     *
     * [feature] Added some cool stuff
     *
     * [changed] Changed some bad stuff
     * ```
     *
     * Will find the following group:
     * ```kotlin
     * "This release contains a known bug. We will address this in a future bugfix."
     * ```
     */
    val SUBTEXT_REGEX = Regex("^([^\\*\\s][\\s\\S]+?)(\\n\\n|(?![\\s\\S]))")

    /**
     * Parses [ReleaseContent] from a [String].
     *
     * For example, the following is a valid string:
     * ```markdown
     * This release contains a known bug. We will address this in a future bugfix.
     *
     * [feature] Added some cool stuff
     *
     * [changed] Changed some bad stuff
     * ```
     *
     * @see CHANGE_REGEX
     * @see SUBTEXT_REGEX
     * @see Change
     */
    fun fromString(string: String): ReleaseContent {
      val changes =
        CHANGE_REGEX.findAll(string).map { Change.fromString(it.firstCapturedValue) }.toList()
      val firstChange = CHANGE_REGEX.find(string)
      val subtext = if (firstChange != null) string.substringBefore(firstChange.value) else string

      return ReleaseContent(subtext.trim(), changes.toList())
    }
  }
}

/**
 * Information about a change that went out in a [release][ReleaseContent].
 *
 * @property type what kind of change it was
 * @property message a description of the change
 * @see fromString
 */
data class Change(val type: ChangeType, val message: String) {

  override fun toString(): String = "- [$type] $message"

  companion object {
    /**
     * Regex for finding the information about a [Change].
     *
     * The regex can be described as such:
     * - Look for a line that might start with a space
     * - Look for a `[` followed by any amount of letters and another `]` (this will be the `type`)
     * - Anything after the `type` should be the `content`
     *
     * For example:
     * ```markdown
     * [feature] Added some cool stuff
     * ```
     *
     * Will find the following named groups:
     * ```kotlin
     * [
     *   "type": "feature",
     *   "content": "Added some cool stuff"
     * ]
     * ```
     */
    val REGEX = Regex("^ ?\\[ ?(?<type>\\w+) ?\\](?<content>[\\s\\S]*)", RegexOption.MULTILINE)

    /**
     * Parses [Change] from a [String].
     *
     * For example, the following is a valid string:
     * ```markdown
     * [feature] Added some cool stuff
     * ```
     *
     * @see REGEX
     * @see ChangeType
     */
    fun fromString(string: String): Change {
      val (type, description) = REGEX.findOrThrow(string).destructured

      return Change(ChangeType.valueOf(type.uppercase()), description.trim())
    }
  }
}

/**
 * A classification for what a change does.
 *
 * For example, does the change add something? Does it add a new feature? Maybe it fixes a bug?
 */
enum class ChangeType {
  FEATURE,
  FIXED,
  CHANGED,
  UNCHANGED,
  REMOVED,
  DEPRECATED;

  override fun toString(): String = name.lowercase()
}
